<html> 
  <head> 
    <title>Benchmark WebGL textured mesh</title> 
 
<!-- Shader code ------------------------------ --> 
 
<script id="vertexshader-c1" type="x-shader/x-vertex"> 
// Minimal shader for testing pure vertex-color fill rate.
#ifdef GL_ES
precision highp float;
#endif

uniform mat4 MVP;
 
attribute vec3 position;
attribute vec3 color;
 
void main() {
  gl_Position = MVP * vec4(position, 1.0);
  gl_FrontColor = vec4(color, 1.0);
  gl_BackColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script> 
 
<script id="fragmentshader-c1" type="x-shader/x-fragment"> 
void main() {
  gl_FragColor = gl_Color;
}
</script> 
 
<script id="vertexshader-t1" type="x-shader/x-vertex"> 
// Minimal shader for testing pure diffuse texture fill rate.
#ifdef GL_ES
precision highp float;
#endif

uniform mat4 MVP;
 
attribute vec3 position;
attribute vec2 t0_uv;
 
varying vec2 uv;
 
void main() {
  gl_Position = MVP * vec4(position, 1.0);
  uv = t0_uv;
}
</script> 
 
<script id="fragmentshader-t1" type="x-shader/x-fragment"> 
#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D t0_sampler;
varying vec2 uv;
 
void main() {
  gl_FragColor = texture2D(t0_sampler, uv);
}
</script> 
 
<!-- Javascript code ------------------------------ --> 
 
    <script src="glmath.js"></script> 
 
    <script> 
// Export the glmath symbols.
for (var k in glmath) {
  window[k] = glmath[k];
}
 
// Float32Array compatibility.
if ('Float32Array' in window) {
  // Leave Float32Array as-is.
} else if ('FloatArray' in window) {
  window.Float32Array = FloatArray;
} else if ('WebGLFloatArray' in window) {
  window.Float32Array = WebGLFloatArray;
} else {
  // Trouble.
  throw "No Float32Array implementation found.";
}
 
// Uint16Array compatibility.
if ('Uint16Array' in window) {
  // Leave Uint16Array as-is.
} else if ('WebGLUnsignedShortArray' in window) {
  window.Uint16Array = WebGLUnsignedShortArray;
} else {
  throw "No Uint16Array implementation found.";
}
 
var gl;
var vshader = {}, fshader = {}, program;
var modelMatrixLocation, modelMatrixGl;
var viewMatrixLocation, viewMatrixGl, viewMatrixJs, viewPosJs;
var projectionMatrixLocation, projectionMatrixGl;
var mvpLocation;
var position_location;
var t0_uv_location;
 
var identityMatrix;
 
var fovy = Math.PI / 3;  // radians
var width, height;
var z_near = 0.1;
var z_far = 20;
var lod_k = 1;
 
var force_grid_div = 0;
 
var MAX_PENDING_LOADS = 16;
var MAX_REQUESTS_PER_FRAME = 4;
 
var draw_repeat_x = 1;
 
// Stats.
var tri_count = 0;
var draw_count = 0;
 
var initialCameraOrient = makeOrientationAffine(
    {x: 5, y: 0, z: 0},  // pos
    {x: -1, y: 0, z: 0},  // dir
    {x: 0, y: 1, z: 0} // up
                                                );
var cameraOrient = {};
copyAffineMatrix(cameraOrient, initialCameraOrient);
 
var world = [];
var uniform_grid = null;
 
var enable_layer = [true, true, true, true, true, true];
 
function el(id) {
  return document.getElementById(id);
}
 
function lerp(x0, x1, t) {
  return (x1 - x0) * t + x0;
}
 
function clamp(x, xmin, xmax) {
  if (x < xmin) {
    return xmin;
  } else if (x > xmax) {
    return xmax;
  }
  return x;
}
 
function dampedDrive(x0, x1, tc, dt) {
  var c0 = Math.exp(-dt / tc);
  return x0 * c0 + x1 * (1 - c0);
}
 
function vectorDampedDrive(v0, v1, tc, dt) {
  var c0 = Math.exp(-dt / tc);
  return vectorAdd(vectorScale(v0, c0), vectorScale(v1, 1 - c0));
}
 
// Tile lookup
// http://khm0.google.com/kh/v=56&x=1&y=2&z=2&s=
// 4.keyhole_maps.khserver.keyhole.yr.borg.google.com/kh/v=51&x=1&y=2&z=2&s=
 
// Globe modeling coords: radius = 1
// x-axis points out of lat:0, lon:0
// y-axis points out of north pole
// z-axis points out of equator/90W intersection (~Galapagos Islands)
 
function latLonDegreesToCartesian(lat, lon) {
  var latRadians = lat * Math.PI / 180;
  var lonRadians = lon * Math.PI / 180;
 
  var y = Math.sin(latRadians);
  var r = Math.cos(latRadians);
  var x = Math.cos(lonRadians) * r;
  var z = Math.sin(-lonRadians) * r;
  return {x: x, y: y, z: z};
}
 
function cartesianToLatLonDegrees(v) {
  var alt = vectorLength(v);
  var vn = vectorNormalize(v);
  var lat = Math.acos(vn.y) * 180 / Math.PI;
  var lon = Math.atan2(-vn.z, vn.x) * 180 / Math.PI;
  return { 'lat': lat, 'lon': lon, 'alt': alt };
}
 
function mercatorLatToY(phi) {
  return Math.log(Math.tan(phi / 2 + Math.PI / 4));
}
 
function mercatorYToLat(y) {
  return 2 * Math.atan(Math.exp(y)) - Math.PI / 2;
}
 
// Lerp in map space.  Inputs & outputs in degrees.
function mercatorLatLerp(lat0, lat1, t) {
  var y0 = mercatorLatToY(lat0 * Math.PI / 180);
  var y1 = mercatorLatToY(lat1 * Math.PI / 180);
  var ymid = lerp(y0, y1, t);
  return mercatorYToLat(ymid) * 180 / Math.PI;
}
 
function latMid(lat0, lat1) {
  return mercatorLatLerp(lat0, lat1, 0.5);
}
 
function lerper(x0, x1, t) {
  return (t - x0) / (x1 - x0);
}
 
function make_grid_triangles(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  // 
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = new Uint16Array((dim - 1) * (dim - 1) * 6);
  var next_i = 0;
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      for (var j = slab_j; j < end_j; j++) {
        var vi = j * dim + i;
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        inds[next_i++] = vi;
        inds[next_i++] = vi + dim + 1;
        inds[next_i++] = vi + dim;
        inds[next_i++] = vi;
        inds[next_i++] = vi + 1;
        inds[next_i++] = vi + dim + 1;
      }
    }
  }
  return inds;
}
 
function make_grid_triangle_strip(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  // 
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = [];
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      var vi = slab_j * dim + i;
      if (inds.length) {
        // Start the strip with a degenerate if we're connecting to a
        // previous strip.
        inds.push(vi + 1);
      }
      inds.push(vi + 1);
      inds.push(vi);
      for (var j = slab_j; j < end_j; j++) {
        //  *--*
        //  |\ | ...
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //   .
        //   .
        //   .
        inds.push(vi + dim + 1);
        inds.push(vi + dim);
        vi += dim;
      }
      // End the strip with a degenerate tri in order to start the
      // next strip.
      inds.push(inds[inds.length - 1]);
    }
  }
  var inds_array = new Uint16Array(inds);
  return inds_array;
}
 
function init_model(model, prim_type, x, y, d, grid_dim) {
  check_gl_error('init_model pre');
 
  var origin = {x: d * -0.15,
                y: (x - 2) * 0.5,
                z: (y - 2) * 0.5};
  var up = {x: 1, y: 0, z: 0}; // vectorNormalize(origin);
  var dir;
  if (Math.abs(up.y) > 0.8) {
    dir = {x: 0, y: 0, z: 1};
  } else {
    dir = {x: 0, y: 1, z: 0};
  }
  var right = crossProduct(dir, up);
  dir = vectorNormalize(crossProduct(up, right));
 
  // Pick some randomish constants for modulating the model.
  var seed = x + y * 27 + d * 129;
  var c0 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 13 + 1) % 8];
  var c1 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 23 + 2) % 8];
  var c2 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 7 + 3) % 8];
  var c3 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 17 + 4) % 8];
  var c4 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 5 + 5) % 8];
  var c5 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 3 + 6) % 8];
  var c6 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 11 + 7) % 8];
  var c7 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 31 + 0) % 8];
  var c8 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 43 + 1) % 8];
  var c9 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 119 + 7) % 8];
  var c10 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 67 + 2) % 8];
  
  var DIM = grid_dim;
  var verts = new Float32Array(DIM * DIM * 5);
  for (var j = 0; j < DIM; j++) {
    for (var i = 0; i < DIM; i++) {
      // Position.
      var u = i / (DIM - 1);
      var v = j / (DIM - 1);
      verts[(j * DIM + i) * 5 + 0] = (u - 0.5) * 0.5;
      verts[(j * DIM + i) * 5 + 1] = (Math.sin(u * c0 * 10 + c1) + Math.cos(v * c2 * 20) + c3) * c4 * 0.15;
      verts[(j * DIM + i) * 5 + 2] = (v - 0.5) * 0.5;
 
      // Texture UV.
      verts[(j * DIM + i) * 5 + 3] = u;
      verts[(j * DIM + i) * 5 + 4] = v;
    }
  }
  var verts_buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, verts_buf);
  gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
 
  check_gl_error('init_model 1');
 
  var inds_array;
  var index_type;
  if (prim_type == "tris") {
    inds_array = make_grid_triangles(DIM);
    index_type = gl.TRIANGLES;
  } else {
    inds_array = make_grid_triangle_strip(DIM);
    index_type = gl.TRIANGLE_STRIP;
  }
  var inds_buf = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, inds_buf);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, inds_array, gl.STATIC_DRAW);
  var index_count = inds_array.length;
 
  check_gl_error('init_model 2');
 
  // Generate a texture.
  var TEX_DIM = 128;
  var imdata = new Uint8Array(TEX_DIM * TEX_DIM * 4);
  for (var j = 0; j < TEX_DIM; j++) {
    for (var i = 0; i < TEX_DIM; i++) {
      var u = i / (TEX_DIM - 1);
      var v = j / (TEX_DIM - 1);
      var vi = (j * TEX_DIM + i) * 4;
      imdata[vi + 0] = Math.abs(Math.sin((u * c5 + c8) * 12)) * 255;    // red
      imdata[vi + 1] = Math.abs(Math.cos((v * c6 + c9) * 16)) * 255;   // green
      imdata[vi + 2] = Math.cos(((u + c7) * (v + c8) + c10) * 100) * 127 + 128;   // blue
      imdata[vi + 3] = 255;   // alpha
    }
  }
  check_gl_error('init_model 2');
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, TEX_DIM, TEX_DIM, 0, gl.RGBA, gl.UNSIGNED_BYTE, imdata);
  gl.generateMipmap(gl.TEXTURE_2D);
  gl.bindTexture(gl.TEXTURE_2D, null);
 
  check_gl_error('init_model 3');
 
  var model_mat = makeOrientationAffine(origin, dir, up);
  var model_mat_gl = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
  affineMatrixToGlMatrix(model_mat, model_mat_gl);
 
  model.matrix_gl = model_mat_gl;
  model.vertices = verts_buf;
  model.indices = inds_buf;
  model.index_count = index_count;
  model.index_type = index_type;
  // gl.LINES -- poor-person's wireframe
  model.triangle_count = (DIM - 1) * (DIM - 1) * 2;
  model.texture = texture;
 
  check_gl_error('init_model post');
}
 
function shutdown_model(model) {
  // Release the pieces.
  gl.deleteTexture(model.texture);
  gl.deleteBuffer(model.vertices);
  gl.deleteBuffer(model.indices);
  check_gl_error('shutdown_model');
}
 
function shutdown_world(world) {
  for (var i = 0; i < world.length; i++) {
    shutdown_model(world[i]);
  }
  world = [];
}
 
function draw_model(model) {
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, model.texture);
 
  gl.bindBuffer(gl.ARRAY_BUFFER, model.vertices);
  gl.vertexAttribPointer(position_location, 3, gl.FLOAT, false, 4 * 5, 0);
  gl.vertexAttribPointer(t0_uv_location, 2, gl.FLOAT, false, 4 * 5, 4 * 3);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indices);
  gl.drawElements(model.index_type, model.index_count,
                  gl.UNSIGNED_SHORT, 0);
  tri_count += model.triangle_count;
  draw_count += 1;
}
 
function draw_world(world) {
  viewMatrixJs = makeViewFromOrientation(cameraOrient);
  affineMatrixToGlMatrix(viewMatrixJs, viewMatrixGl);
  //var viewMatrixLocation = gl.getUniformLocation(program, 'viewMatrix');
  //gl.uniformMatrix4fv(viewMatrixLocation, false, viewMatrixGl);
 
  var vp = myMatrixMultiply(projectionMatrix, viewMatrixGl);
 
  for (var i = 0; i < world.length; i++) {
    var model = world[i];
    var mvp = myMatrixMultiply(vp, model.matrix_gl);
    gl.uniformMatrix4fv(mvpLocation, false, mvp);
    draw_model(model);
  }
  check_gl_error('draw_world');
}
 
function get_context(canvas) {
  var gl;
  try { gl = canvas.getContext("webkit-3d"); } catch(e) {}
  if(!gl) try { gl = canvas.getContext("moz-webgl"); } catch(e) {}
  if(!gl) try { gl = canvas.getContext("experimental-webgl"); } catch(e) {}
  if(!gl) try { gl = canvas.getContext("webgl"); } catch(e) {}
  if (gl) {
    check_gl_error('get_context');
  }
  return gl;
}
 
function check_gl_error(tag) {
  if (!gl) { return; }
  var error = gl.getError();
  if (error != gl.NO_ERROR) {
    var str = 'GL Error: ' + error + ' (' + tag + ')';
    if (console) {
      console.log(str);
    }
    debugger;
    throw str;
  }
}
 
function load_shader(id, type) {
  var shader_el = el(id);
  var shader = gl.createShader(type);
  if (!shader) return null;
  gl.shaderSource(shader, shader_el.text);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    if (console) {
      console.log("Error compiling shader: " +
		  gl.getShaderInfoLog(shader));
    }
    return null;
  }
  check_gl_error('load_shader');
  return shader;
}
 
// Return a new WebGLFloatArray w/ the appropriate values.
//
// fovy in radians
// aspect ratio is x/y
function myGluPerspective(fovy, aspect, z_near, z_far) {
  var f = 1 / Math.tan(fovy / 2);
  return new Float32Array([
      f / aspect, 0, 0, 0,
      0, f, 0, 0,
      0, 0, (z_far + z_near) / (z_near - z_far), -1,
      0, 0, 2 * z_far * z_near / (z_near - z_far), 0
                               ]);
}
 
function myMatrixMultiply(ma, mb) {
  var result = new Float32Array(16);
  for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
      var val = 0;
      for (var k = 0; k < 4; k++) {
	val += ma[j + k * 4] * mb[k + i * 4];
      }
      result[j + i * 4] = val;
    }
  }
  return result;
}
 
function reshape(canvas, w, h) {
  width = w;
  height = h;
 
  gl.viewport(0, 0, w, h);
 
  lod_k = h / (2 * Math.tan(fovy / 2));
}
 
function init_gl() {
  try {
    var canvas = el('canvas');
    gl = get_context(canvas);
    if (!gl) {
      return null;
    }
 
    vshader.t1 = load_shader('vertexshader-t1', gl.VERTEX_SHADER);
    if (!vshader.t1) return null;
 
    fshader.t1 = load_shader('fragmentshader-t1', gl.FRAGMENT_SHADER);
    if (!fshader.t1) return null;
 
    program = gl.createProgram();
    if (!program) return null;
 
    gl.attachShader(program, vshader.t1);
    gl.attachShader(program, fshader.t1);
 
    gl.bindAttribLocation(program, 0, 'position');
    //  gl.bindAttribLocation(program, 1, 'texcoord_in');
 
    gl.linkProgram(program);
 
    check_gl_error('init_gl 1');
 
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      if (console) {
        console.log('Error in program linking:' + gl.getProgramInfoLog(program));
      }
      return null;
    }
 
    gl.useProgram(program);
 
    check_gl_error('init_gl 2');
 
    modelMatrixLocation = gl.getUniformLocation(program, 'modelMatrix');
    viewMatrixLocation = gl.getUniformLocation(program, 'viewMatrix');
    projectionMatrixLocation = gl.getUniformLocation(program, 'projectionMatrix');
    mvpLocation = gl.getUniformLocation(program, 'MVP');
    modelMatrixGl = new Float32Array([
                                         1, 0, 0, 0,
                                         0, 1, 0, 0,
                                         0, 0, 1, 0,
                                         0, 0, 0, 1,
                                      ]);
    viewMatrixGl = new Float32Array([
                                        1, 0, 0, 0,
                                        0, 1, 0, 0,
                                        0, 0, 1, 0,
                                        0, 0, 0, 1,
                                     ]);
    identityMatrix = new Float32Array([
                                          1, 0, 0, 0,
                                          0, 1, 0, 0,
                                          0, 0, 1, 0,
                                          0, 0, 0, 1,
                                       ]);
 
    check_gl_error('init_gl 3');
 
    gl.uniform1i(gl.getUniformLocation(program, 't0_sampler'), 0);
 
    check_gl_error('init_gl 4');
 
    reshape(canvas, canvas.clientWidth, canvas.clientHeight);
 
    position_location = gl.getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(position_location);
    t0_uv_location = gl.getAttribLocation(program, 't0_uv');
    gl.enableVertexAttribArray(t0_uv_location);
 
    gl.clearColor(0, 0, 0, 1);
    gl.clearDepth(1.0);
 
    check_gl_error('init_gl post');
    return gl;
  } catch (err) {
    gl = null;
    if (console) {
      console.log(err);
    }
    return null;
  }
}
 
 
function draw() {
  projectionMatrix = myGluPerspective(fovy, width / height, z_near, z_far);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  gl.depthFunc(gl.LEQUAL);
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.BACK);
 
  draw_world(world, matrixColumn(cameraOrient, 3));
 
  check_gl_error('draw');
}
 
var start, last_timestamp = 0;
var metrics;
var frame_number = 0;
var paused = true;
var refresh_timer = -1;
 
var canvas_elem;
var canvas_x, canvas_y;
var zoom_in_pressed = false;
var zoom_out_pressed = false;
var up_pressed = false;
var down_pressed = false;
var right_pressed = false;
var left_pressed = false;
var shift_pressed = false;
var ctrl_pressed = false;
var alt_pressed = false;
var mouse_is_down = false;
var right_mouse_is_down = false;
var last_right_mouse_down = 0;
var mouse_x = 0;
var mouse_y = 0;
var mouse_down_x = 0;
var mouse_down_y = 0;
var mouse_grab_point = null;
var mouse_double_clicked = false;
var right_mouse_double_clicked = false;
var globe_omega = { x: 0, y: 0, z: 0 };
 
var tangent_dir = { x: 0, y: 1, z: 0 };
var anchor = { x: 0, y: 0, z: 1 };
var target_anchor = { x: 1, y: 0, z: 0 };
var pitch = -Math.PI / 2;
var target_pitch = -Math.PI / 2;
var initialTargetDist = 1.3;
var target_dist = initialTargetDist;
var dist = 5;
 
function keydown(e) {
  set_paused(false);
  if (event.keyCode == 33) {  // PgUp
    zoom_in_pressed = true;
  } else if (event.keyCode == 34) {  // PgDn
    zoom_out_pressed = true;
  } else if (event.keyCode == 38) {  // Up
    up_pressed = true;
  } else if (event.keyCode == 40) {  // Down
    down_pressed = true;
  } else if (event.keyCode == 39) {  // Right
    right_pressed = true;
  } else if (event.keyCode == 37) {  // Left
    left_pressed = true;
  } else if (event.keyCode == 16) {  // Shift
    shift_pressed = true;
  } else if (event.keyCode == 17) {  // Ctrl
    ctrl_pressed = true;
  } else if (event.keyCode == 18) {  // Alt
    alt_pressed = true;
  } else if (event.keyCode >= 48 && event.keyCode < 58) {  // number keys
    var num = event.keyCode - 48;
    // Toggle visibility.
    if (num < 6) {
      enable_layer[num] = !enable_layer[num];
    }
  }
}
 
function keyup(e) {
  if (event.keyCode == 33) {  // PgUp
    zoom_in_pressed = false;
  } else if (event.keyCode == 34) {  // PgDn
    zoom_out_pressed = false;
  } else if (event.keyCode == 38) {  // Up
    up_pressed = false;
  } else if (event.keyCode == 40) {  // Down
    down_pressed = false;
  } else if (event.keyCode == 39) {  // Right
    right_pressed = false;
  } else if (event.keyCode == 37) {  // Left
    left_pressed = false;
  } else if (event.keyCode == 16) {  // Shift
    shift_pressed = false;
  } else if (event.keyCode == 17) {  // Ctrl
    ctrl_pressed = false;
  } else if (event.keyCode == 18) {  // Alt
    alt_pressed = false;
  }
}
 
function rememberMousePos(e) {
  var width = canvas_elem.width;
  var height = canvas_elem.height;
  mouse_x = ((e.pageX - canvas_x) - width / 2) / (height / 2);
  mouse_y = -(((e.pageY - canvas_y) - height / 2) / (height / 2));
}
 
function mousedown(e) {
  mouse_is_down = (e.which == 1);
  right_mouse_is_down = (e.which == 3);
  if (right_mouse_is_down) {
    var timestamp = new Date().getTime();
    if (timestamp - last_right_mouse_down < 250) {
      right_mouse_double_clicked = true;
    }
    last_right_mouse_down = timestamp;
  }
  rememberMousePos(e);
  mouse_down_x = mouse_x;
  mouse_down_y = mouse_y;
  e.preventDefault();
}
 
function mouseclick(e) {
  e.stopPropagation();
  e.preventDefault();
  return false;
}
 
function mousedblclick(e) {
  if (ctrl_pressed) {
    right_mouse_double_clicked = true;
  } else {
    mouse_double_clicked = true;
  }
  rememberMousePos(e);
  set_paused(false);
//   e.preventDefault();
}
 
function mousemove(e) {
  rememberMousePos(e);
  if (mouse_is_down || right_mouse_is_down) {
    set_paused(false);
  }
}
 
function mouseup(e) {
  if (e.which == 1) {
    mouse_is_down = false;
  }
  if (e.which == 3) {
    right_mouse_is_down = false;
    set_paused(false);
  }
}
 
// Return the first exterior hit or closest point between the unit
// sphere and the ray starting at p and going in the r direction.
function rayVsUnitSphereClosestPoint(p, r) {
  var p_len2 = dotProduct(p, p);
  if (p_len2 < 1) {
    // Ray is inside sphere, no exterior hit.
    return null;
  }
 
  var along_ray = -dotProduct(p, r);
  if (along_ray < 0) {
    // Behind ray start-point.
    return null;
  }
 
  var perp = vectorAdd(p, vectorScale(r, along_ray));
  var perp_len2 = dotProduct(perp, perp);
  if (perp_len2 >= 0.999999) {
    // Return the closest point.
    return vectorNormalize(perp);
  }
 
  // Compute intersection.
  var e = Math.sqrt(1 - dotProduct(perp, perp));
  var hit = vectorAdd(p, vectorScale(r, (along_ray - e)));
  return vectorNormalize(hit);
}
 
function screenToSpherePt(x, y) {
  var p = matrixColumn(cameraOrient, 3);
  // camera dir
  var r = matrixColumn(cameraOrient, 0);
  var up = matrixColumn(cameraOrient, 1);
  var right = matrixColumn(cameraOrient, 2);
  var tan_half = Math.tan(fovy / 2);
  r = vectorAdd(r, vectorScale(right, x * tan_half));
  r = vectorAdd(r, vectorScale(up, y * tan_half));
  r = vectorNormalize(r);
 
  return rayVsUnitSphereClosestPoint(p, r);
}
 
function rotateGlobe(scaled_axis) {
  var angle = -Math.asin(Math.sqrt(dotProduct(scaled_axis, scaled_axis)));
  var axis = vectorNormalize(scaled_axis);
  var mat = makeRotateAxisAngle(axis, angle);
  anchor = applyRotation(mat, anchor);
  target_anchor = vectorNormalize(applyRotation(mat, target_anchor));
  tangent_dir = applyRotation(mat, tangent_dir);
}
 
function update(dt) {
  // Navigation.
  var MIN_DIST = 5e-6;
  var MAX_DIST = 10;
  var zoom_speed = lerp(1e-5, 10, lerper(MIN_DIST, MAX_DIST, dist));
  var keep_running = false;
 
  if (zoom_in_pressed) {
    target_dist -= zoom_speed * dt;
  }
  if (zoom_out_pressed) {
    target_dist += zoom_speed * dt;
  }
 
  if (mouse_double_clicked) {
    mouse_double_clicked = false;
    target_anchor = screenToSpherePt(mouse_x, mouse_y);
    target_dist /= 2;
  }
  if (right_mouse_double_clicked && !right_mouse_is_down) {
    right_mouse_double_clicked = false;
    target_anchor = screenToSpherePt(mouse_x, mouse_y);
    target_dist *= 2;
  }
 
  if (shift_pressed) {
    // Pitch control.
    if (up_pressed) {
      target_pitch -= 1 * dt;
      if (target_pitch < -Math.PI / 2) {
        target_pitch = -Math.PI / 2;
      }
    }
    if (down_pressed) {
      target_pitch += 1 * dt;
      if (target_pitch > 0) {
        target_pitch = 0;
      }
    }
    // Roll control.
    if (left_pressed) {
      var mat = makeRotateAxisAngle(anchor, 1 * dt);
      tangent_dir = applyRotation(mat, tangent_dir);
    }
    if (right_pressed) {
      var mat = makeRotateAxisAngle(anchor, -1 * dt);
      tangent_dir = applyRotation(mat, tangent_dir);
    }
  } else {
    // Move the anchor.
    if (left_pressed) {
      rotateGlobe(vectorScale(tangent_dir, 1 * dt * zoom_speed));
    }
    if (right_pressed) {
      rotateGlobe(vectorScale(tangent_dir, -1 * dt * zoom_speed));
    }
    if (up_pressed || down_pressed) {
      var right_axis = crossProduct(tangent_dir, anchor);
      if (up_pressed) {
        rotateGlobe(vectorScale(right_axis, 1 * dt * zoom_speed));
      }
      if (down_pressed) {
        rotateGlobe(vectorScale(right_axis, -1 * dt * zoom_speed));
      }
    }
  }
 
  if (mouse_is_down) {
    var new_grab_point = screenToSpherePt(mouse_x, mouse_y);
    if (mouse_grab_point == null && new_grab_point != null) {
      mouse_grab_point = new_grab_point;
    }
    if (mouse_grab_point && new_grab_point) {
      // Rotate the earth, to map old grab point onto new grab point.
      var axis = crossProduct(mouse_grab_point, new_grab_point);
      axis = vectorScale(axis, 0.85);
      rotateGlobe(axis);
 
      globe_omega = vectorScale(axis, 1000 / dt);
    }
    keep_running = true;
  } else {
     // Momentum spin.
    mouse_grab_point = null;
 
    globe_omega = vectorDampedDrive(globe_omega, {x:0, y:0, z:0}, 0.95, dt);
    var axis = vectorScale(globe_omega, dt / 1000);
    rotateGlobe(axis);
  }
 
  if (right_mouse_is_down) {
    // Zoom in/out.
    var zoom_factor = 2 * (mouse_down_y - mouse_y);
    target_dist += zoom_factor * dt * zoom_speed;
 
    // Rotate.
    var angle_factor = 2 * (mouse_down_x - mouse_x);
    var mat = makeRotateAxisAngle(anchor, angle_factor * dt);
    tangent_dir = applyRotation(mat, tangent_dir);
 
    keep_running = true;
  }
 
  target_dist = clamp(target_dist, MIN_DIST, MAX_DIST);
 
  // Make cameraOrient based on params.
  dist = dampedDrive(dist, target_dist, 0.10, dt);
  pitch = dampedDrive(pitch, target_pitch, 0.10, dt);
  anchor = vectorNormalize(vectorDampedDrive(anchor, target_anchor, 0.10, dt));
  // Flatten the tangent to keep it tangent.
  tangent_dir = vectorNormalize(
      vectorSub(tangent_dir,
                vectorScale(anchor, dotProduct(anchor, tangent_dir))));
 
  var pos = vectorAdd(vectorScale(anchor, 1 - dist * Math.sin(pitch)),
                      vectorScale(tangent_dir, - dist * Math.cos(pitch)));
  var dir = vectorNormalize(vectorSub(anchor, pos));
  var right = crossProduct(tangent_dir, anchor);
  var up = crossProduct(right, dir);
  cameraOrient = makeOrientationAffine(pos, dir, up);
 
  if (left_pressed || right_pressed || up_pressed || down_pressed ||
      zoom_in_pressed || zoom_out_pressed ||
      Math.abs(dist - target_dist) > 1e-6 ||
      Math.abs(pitch - target_pitch) > 1e-5 ||
      vectorLength(vectorSub(anchor, target_anchor)) > 1e-6 ||
      dotProduct(globe_omega, globe_omega) > 0.000001) {
    keep_running = true;
  }
 
  if (!keep_running) {
    globe_omega = {x: 0, y: 0, z: 0};
    set_paused(true);
  }
 
  normalizeMatrixRotation(cameraOrient);
 
  // Adjust z_near to pull it in close when camera is near the ground
  // (to avoid near-clip); push it out when camera is far away (to
  // avoid z-fighting).
  var normalized_altitude = vectorLength(matrixColumn(cameraOrient, 3));
  z_near = lerp(6e-7, 1e-1, Math.log(normalized_altitude) / Math.log(5));
 
  // Update our cached values of the location of the canvas on the
  // page (we use this for interpreting mouse input).
  canvas_x = 0;
  canvas_y = 0;
  var elem = canvas_elem;
  while (elem) {
    canvas_x += elem.offsetLeft;
    canvas_y += elem.offsetTop;
    elem = elem.offsetParent;
  }
}
 
function init_world(depth_x, dim, prim_type) {
  check_gl_error('init_world pre');
  world = [];
  for (var d = 0; d < depth_x; d++) {
    for (var j = 0; j < 5; j++) {
      for (var i = 0; i < 5; i++) {
        var model = {};
        init_model(model, prim_type, i, j, d, dim);
        world.push(model);
      }
    }
  }
  check_gl_error('init_world post');
}
 
var report;
 
function generate_report() {
  // Put the camera in a standard place.
  cameraOrient = makeOrientationAffine(
    {x: 2.3, y: 0, z: 0},  // pos
    {x: -1, y: 0, z: 0},  // dir
    {x: 0, y: 1, z: 0} // up
                                           );
  set_paused(true);
  el('gen_report').disabled = true;
  el('report_status').innerHTML = 'STARTING';
  report = [];
  setTimeout(function() { report_sequence(0); }, 0);
}
 
function report_sequence(seq) {
  var tests_params = [
      {layers: 4, dim: 16, prim: 'strip'},
      {layers: 4, dim: 32, prim: 'strip'},
      {layers: 4, dim: 64, prim: 'strip'},
      {layers: 8, dim: 64, prim: 'strip'},
                         ];
 
  if (seq >= tests_params.length) {
    // Done.
    el('gen_report').disabled = false;
    el('report_status').innerHTML = 'DONE';
    show_report();
    return;
  }
 
  // Do a test.
  el('report_status').innerHTML = 'WORKING: step ' + (seq + 1) + ' / ' + tests_params.length;
  var params = tests_params[seq];
 
  init_world(params.layers, params.dim, params.prim);
 
  // Warm up.
  for (var i = 0; i < 10; i++) {
    draw();
  }
 
  // Do a bunch of draws -- run for at least two seconds.
  tri_count = 0;
  draw_count = 0;
  var start = (new Date()).getTime();
 
  for (;;) {
    for (var i = 0; i < 10; i++) {
      draw();
    }
    var now = (new Date()).getTime();
    if (now - start > 2000) {
      break;
    }
  }
 
  var end = (new Date()).getTime();
  var dt = end - start;
 
  // Compute rates.
  var tps = Math.round(1000 * tri_count / dt);
  var dps = Math.round(1000 * draw_count / dt);
 
  report.push({layers: params.layers, dim: params.dim, prim: params.prim,
	dt: dt, tps: tps, dps: dps});
  setTimeout(function() { report_sequence(seq + 1); }, 100);
}
 
function pad(x, width) {
  var text = '' + x;
  while (text.length < width) {
    text = ' ' + text;
  }
  return text;
}
 
function get_param(x) {
  if (gl.getString) {
    return gl.getString(x);
  } else {
    return gl.getParameter(x);
  }
}
 
function show_report() {
  var text = '';
  text += 'userAgent = ' + navigator.userAgent + '\n';
  text += 'gl.VERSION = ' + get_param(gl.VERSION) + '\n';
  text += 'gl.VENDOR = ' + get_param(gl.VENDOR) + '\n';
  text += 'gl.RENDERER = ' + get_param(gl.RENDERER) + '\n';
  text += 'gl.SHADING_LANGUAGE_VERSION = ' + get_param(gl.SHADING_LANGUAGE_VERSION) + '\n';
  text += '+--------+-----+-------+------------+-----------+---------+\n';
  text += '| layers | dim | prim  |  tris/sec  | draws/sec |   dt    |\n';
  text += '+--------+-----+-------+------------+-----------+---------+\n';
  for (var i = 0; i < report.length; i++) {
    var rep = report[i];
    var line = '| ' + pad(rep.layers, 6) + ' | ' + pad(rep.dim, 3) + ' | ' + pad(rep.prim, 5) +
               ' | ' + pad(rep.tps, 10) + ' | ' + pad(rep.dps, 9) + ' | ' + pad(rep.dt / 1000, 7) + ' |\n';
    text += line;
  }
  text += '+--------+-----+-------+------------+-----------+---------+\n';
 
  el('report').value = text;
 
  reinit_world();
  set_paused(false);
}
 
function reinit_world() {
  init_world(parseInt(el('sb_depth_x').value),
             parseInt(el('sb_grid_dim').value),
             el('sb_prim').value);
}
 
function start() {
  init_gl();
  if (!gl) {
    el('init_failure').style.display = '';
    return;
  }
 
  reinit_world();
 
  start = new Date().getTime();
  var met = {timestamp: start, draws: 0, tris: 0};
  metrics = [met, met, met, met, met, met, met, met, met, met, met, met];
  frame_number = 0;
 
  set_paused(false);
 
  canvas_elem = el('canvas');
  canvas_elem.addEventListener('mousedown', mousedown, false);
  canvas_elem.addEventListener('click', mouseclick, false);
  canvas_elem.addEventListener('dblclick', mousedblclick, false);
  document.addEventListener('mousemove', mousemove, false);
  document.addEventListener('mouseup', mouseup, false);
  document.body.addEventListener('keydown', keydown, false);
  document.body.addEventListener('keyup', keyup, false);
 
  // Disable the context menu over the canvas so we can process right-clicks.
  canvas_elem.addEventListener('contextmenu', function(e) {
      e.preventDefault(); }, false);
 
  var change_params = function(e) {
    shutdown_world(world);
    reinit_world();
    // copyAffineMatrix(cameraOrient, initialCameraOrient);
    // target_dist = initialTargetDist;
    set_paused(false);
    el('sb_grid_dim').blur();
    el('sb_depth_x').blur();
    el('sb_prim').blur();
  };
  el('sb_grid_dim').addEventListener('change', change_params);
  el('sb_depth_x').addEventListener('change', change_params);
  el('sb_prim').addEventListener('change', change_params);
 
  el('sb_draw_x').addEventListener('change', function(e) {
      draw_repeat_x = parseInt(el('sb_draw_x').value);
      el('sb_draw_x').blur();
    }, false);
 
  el('gen_report').addEventListener('click', generate_report);
 
//   document.getElementById('cb_pause').addEventListener('change', function(e) {
//       set_paused(this.checked);
//     });
}
 
function refresh_function() {
  var timestamp = new Date().getTime();
  var dt = (timestamp - last_timestamp) / 1000.0;
  last_timestamp = timestamp;
 
  tri_count = 0;
  draw_count = 0;
 
  update(dt);
  for (var i = 0; i < draw_repeat_x; i++) {
    draw();
  }
 
  var met = {timestamp: timestamp, draws: draw_count, tris: tri_count};
  metrics[frame_number % metrics.length] = met;
  var frame_ct = 0;
  var frame_ms = 0;
  var total_draws = 0;
  var total_tris = 0;
  for (var i = metrics.length; i > 1; i--) {
    var met0 = metrics[(frame_number + i) % metrics.length];
    var met_minus_1 = metrics[(frame_number + i - 1) % metrics.length];
    if (met0 && met_minus_1) {
      var t0 = met0.timestamp;
      var t_minus_1 = met_minus_1.timestamp;
      frame_ms += t0 - t_minus_1;
      frame_ct++;
      total_draws += met0.draws;
      total_tris += met0.tris;
    }
  }
  if (frame_ms > 0) {
    var fps = 1000 * frame_ct / frame_ms;
    el('fps').innerHTML = Math.round(fps);
 
    var tps = 1000 * total_tris / frame_ms;
    el('tps').innerHTML = Math.round(tps);
 
    var dps = 1000 * total_draws / frame_ms;
    el('dps').innerHTML = Math.round(dps);
  }
  frame_number++;
 
  el('tri_count').innerHTML = tri_count;
  el('draw_count').innerHTML = draw_count;
}
 
function moreless(classname) {
  var ml = el('moreless_' + classname);
  var els = document.getElementsByClassName(classname);
  var vis = -1;
  for (var i = 0; i < els.length; i++) {
    var ci = els[i];
    if (vis == -1) {
      if (ci.style.display == 'none') {
        vis = 1;
      } else {
        vis = 0;
      }
    }
    if (vis == 1) {
      ci.style.display = '';
    } else {
      ci.style.display = 'none';
    }
  }
  if (vis == 1) {
    ml.innerHTML = '&laquo;';
  } else {
    ml.innerHTML = '&raquo;';
  }
}
 
function set_paused(new_paused) {
  if (paused == new_paused) {
    return;
  }
  paused = new_paused;
 
  if (refresh_timer != -1) {
    clearInterval(refresh_timer);
    refresh_timer = -1;
  }
 
  if (!paused) {
    refresh_timer = setInterval(refresh_function, 1 /*16*/);
    last_timestamp = new Date().getTime();
  } else {
    metrics[frame_number % metrics.length] = null;
    frame_number++;
  }
}
 
function resize(w, h) {
  var canvas = document.getElementById('canvas');
  var gl = get_context(canvas);
 
  canvas.setAttribute('width', w + 'px');
  canvas.setAttribute('height', h + 'px');
  reshape(canvas, w, h);
}
 
    </script> 
  </head> 
 
  <body onload="start()"> 
 
    <a href="#" onclick="moreless('chrome')"
       id="moreless_chrome">&laquo;</a> 
    <div class="chrome"> 
      <h3>Benchmark WebGL textured mesh</h3> 
 
      <p>Generated unique static grid meshes, each with its own unique 128x128 texture.</p> 
 
      <div id="init_failure" style="display:none; background-color: #ffff00;"> 
        <b>Unable to initialize WebGL!</b> 
      </div> 
 
      <div>Compatibility info <a href="#" onclick="moreless('compat_info')"
                                 id="moreless_compat_info">&raquo;</a></div> 
      <div class="compat_info" style="display: none;"> 
 
        <p>Works in nightly versions of FF, Chrome & Safari, when
          configured correctly;
          <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">see
            here for links to nightlies</a>.</p> 
 
      </div> 
    </div> 
 
    <div class="chrome"> 
      <!-- <span><input type="checkbox" id="cb_pause" value="pause" />pause</span> --> 
      <table border=1> 
        <tr> 
          <td width=100 align=center>fps</td> 
          <td width=100 align=center>tris/sec</td> 
          <td width=100 align=center>draws/sec</td> 
          <td width=100 align=center>draws</td> 
          <td width=100 align=center>tris</td> 
        </tr> 
        <tr> 
          <td align=right><span style="font-family: monospace;" id="fps">--</span></td> 
          <td align=right><span style="font-family: monospace;" id="tps"></span></td> 
          <td align=right><span style="font-family: monospace;" id="dps"></span></td> 
          <td align=right><span style="font-family: monospace;" id="draw_count"></span></td> 
          <td align=right><span style="font-family: monospace;" id="tri_count"></span></td> 
        </tr> 
      </table> 
      <table> 
        <tr> 
          <td> 
            <span>layers: <select id="sb_depth_x"> 
                <option value="1">1</option> 
                <option value="2">2</option> 
                <option selected value="4">4</option> 
                <option value="8">8</option> 
                <option value="16">16</option> 
                <option value="32">32</option> 
              </select> 
            </span><br/> 
          </td> 
          <td> 
            <span>model grid: <select id="sb_grid_dim"> 
                <option value="4">4x4</option> 
                <option value="8">8x8</option> 
                <option value="16">16x16</option> 
                <option value="32">32x32</option> 
                <option selected value="64">64x64</option> 
              </select> 
            </span><br/> 
          </td> 
          <td> 
            <span>draw repeat x: <select id="sb_draw_x"> 
                <option selected value="1">1</option> 
                <option value="2">2</option> 
                <option value="4">4</option> 
                <option value="8">8</option> 
                <option value="16">16</option> 
                <option value="32">32</option> 
                <option value="64">64</option> 
                <option value="128">128</option> 
              </select> 
            </span> 
          </td> 
          <td> 
            <span>prim: <select id="sb_prim"> 
                <option value="tris">indexed tris</option> 
                <option selected value="strip">indexed tristrip</option> 
              </select> 
            </span> 
          </td> 
        </tr> 
      </table> 
    </div> 
 
    <table border=0> 
      <tr> 
        <td valign="top"> 
          <canvas id="canvas" width="640" height="480" style="margin: 3px; border: 1px dashed red"></canvas> 
        </td> 
        <td valign="top" class="chrome"> 
          <a href="#" onclick="moreless('notes')"
             id="moreless_notes">&laquo;</a> 
          <div class="notes chrome"> 
            <ul> 
              <li>left mouse drag: move</li> 
              <li>left double-click: zoom in</li> 
              <li>right double-click: zoom out</li> 
              <li>ctrl + left double-click: zoom out</li> 
              <li>right mouse drag: zoom in/out</li> 
              <li>arrow keys: rotate</li> 
              <li>shift + arrow keys: tilt/rotate view</li> 
              <li>pgup + pgdn keys: zoom in/out</li> 
            </ul> 
 
            <h4>Notes:</h4> 
            <ul> 
              <li>The slowness of making a report is mostly due to the
              time it takes to generate new meshes & textures for each
              set of parameters.</li> 
            </ul> 
          </div> 
        </td> 
      </tr> 
    </table> 
 
    <a href="#" onclick="moreless('report_info')" id="moreless_report_info">&laquo;</a> 
    <div class="report_info"> 
      <input type="button" id="gen_report" value="gen report (be patient)"/> 
      Report: <span id="report_status"></span><br/> 
      <textarea id="report" cols="80" rows="16" style="font-family: monospace;"> 
      </textarea> 
    </div> 
 
  </body> 
</html>
